﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;

namespace SharpGeo.IO.ShapeFiles
{
    /// <summary>
    /// A static class to read ersi shapefile
    /// </summary>
    public static class ShapeFile
    {
        #region Read Methods

        /// <summary>
        /// Reads a shape file from a file
        /// </summary>
        /// <param name="file">The file to read</param>
        /// <param name="deferredLoad">True if the record should not be loaded immediately</param>
        /// <returns>A ShapeFileGraph</returns>
        public static ShapeFileGraph Read(string file, bool deferredLoad)
        {
            if (!File.Exists(file))
                return null;

            using (var f = File.OpenRead(file))
            {
                return Read(f, deferredLoad);
            }
        }

        /// <summary>
        /// Reads a shape file from a stream
        /// </summary>
        /// <param name="stream">The stream to read the shapefile from</param>
        /// <param name="deferredLoad">True if the record should not be loaded immediately</param>
        /// <returns>A ShapeFileGraph</returns>
        public static ShapeFileGraph Read(Stream stream, bool deferredLoad)
        {
            if (stream == null || !(stream.CanRead && stream.CanSeek))
                return null;

            //read in the header
            byte[] header = new byte[100];

            if (stream.Read(header, 0, 100) != 100)
                return null;

            // match the file code to confirm this is a shapefile
            if (!(header[0] == 0x00 && header[1] == 0x00 && header[2] == 0x27 && header[3] == 0x0a))
                return null;

            var fileVersion = ByteConverter.ToLittleEndianInt32(header, 28);
            if (fileVersion != 1000)
                return null;

            var shapeType = ByteConverter.ToLittleEndianInt32(header, 32);

            if (!Enum.IsDefined(typeof(ShapeType), shapeType))
                return null;

            var graph = new ShapeFileGraph()
            {
                ShapeType = (ShapeType)shapeType,
                Envelope = new Envelope(
                    ByteConverter.ToLittleEndianDouble(header, 36),
                    ByteConverter.ToLittleEndianDouble(header, 44),
                    ByteConverter.ToLittleEndianDouble(header, 52),
                    ByteConverter.ToLittleEndianDouble(header, 60))
            };

            if (IsZType(graph.ShapeType))
            {
                graph.MinimumZ = ByteConverter.ToLittleEndianDouble(header, 68);
                graph.MaximumZ = ByteConverter.ToLittleEndianDouble(header, 76);
            }

            if (IsMeasured(graph.ShapeType))
            {
                graph.MinimumM = ByteConverter.ToLittleEndianDouble(header, 84);
                graph.MaximumM = ByteConverter.ToLittleEndianDouble(header, 92);
            }

            //grap the offsets
            do
            {
                var layerHeader = new byte[12];

                //if we cannot read the layer header, the file is corrupt
                if (stream.Read(layerHeader, 0, 12) != 12)
                    return null;

                var recordType = ByteConverter.ToLittleEndianInt32(header, 32);

                if (!Enum.IsDefined(typeof(ShapeType), recordType))
                    return null;

                var layerSize = (long)ByteConverter.ToBigEndianInt32(layerHeader, 4) * (long)2;

                graph.AddRecord(new ShapeFileRecordGraph()
                {
                    ShapeType = (ShapeType)recordType,
                    FileOffset = stream.Position - 4,
                    RecordSize = layerSize
                });

                //if there are no more records then we will break
                if (!((stream.Position + layerSize - 4) < stream.Length))
                    break;

                //otherwise advance the stream until the end
                stream.Seek(layerSize - 4, SeekOrigin.Current);
            } while (true);

            if (deferredLoad)
                return graph;

            for (int i = 0; i < graph.Records.Count; i++)
            {
                LoadShapeFileRecord(stream, graph.Records[i]);
            }

            return graph;
        }

        /// <summary>
        /// Loads a ShapeFile Record
        /// </summary>
        /// <param name="stream">The stream to load the record from</param>
        /// <param name="graph">The ShapeFileRecordGraph to load</param>
        public static void LoadShapeFileRecord(Stream stream, ShapeFileRecordGraph graph)
        {
            if (
                  stream == null ||
                  graph == null ||
                !(stream.CanRead && stream.CanSeek))
                return;

            stream.Seek(graph.FileOffset + 4, SeekOrigin.Begin);

            switch (graph.ShapeType)
            {
                case ShapeType.Point:
                    graph.Data = LoadPoint(1, 2, stream);
                    break;

                case ShapeType.Polygon:
                case ShapeType.Polyline:
                    graph.Data = LoadPolyShape(stream, graph.RecordSize, 2, false);
                    break;

                case ShapeType.Multipoint:
                    graph.Data = LoadMultiPoint(stream, graph.RecordSize, 2);
                    break;

                case ShapeType.PointZ:
                    graph.Data = LoadPoint(1, 4, stream);
                    break;

                case ShapeType.PolylineZ:
                case ShapeType.PolygonZ:
                    graph.Data = LoadPolyShape(stream, graph.RecordSize, 4, false);
                    break;

                case ShapeType.MultiPointZ:
                    graph.Data = LoadMultiPoint(stream, graph.RecordSize, 4);
                    break;

                case ShapeType.PointM:
                    graph.Data = LoadPoint(1, 3, stream);
                    break;

                case ShapeType.PolylineM:
                case ShapeType.PolygonM:
                    graph.Data = LoadPolyShape(stream, graph.RecordSize, 3, false);
                    break;

                case ShapeType.MultiPointM:
                    graph.Data = LoadMultiPoint(stream, graph.RecordSize, 3);
                    break;

                case ShapeType.MultiPatch:
                    graph.Data = LoadPolyShape(stream, graph.RecordSize, 4, true);
                    break;

                case ShapeType.Unknown:
                case ShapeType.Null:
                default:
                    break;
            }
        }

        #endregion Read Methods

        #region Record Loading Methods

        private static ShapeFilePoint LoadMultiPoint(Stream stream, long recordSize, int numberOfAxes)
        {
            //ignore the bounding box
            stream.Seek(32, SeekOrigin.Current);

            var bytes = new byte[4];
            stream.Read(bytes, 0, bytes.Length);
            var numberOfPoints = ByteConverter.ToLittleEndianInt32(bytes, 0);

            //read in the points
            var points = new byte[8 * numberOfPoints * 2];
            stream.Read(points, 0, points.Length);

            byte[] zAxis = null;

            if (numberOfAxes == 4)
            {
                //ignore the Z Range
                stream.Seek(16, SeekOrigin.Current);

                zAxis = new byte[8 * numberOfPoints];
                stream.Read(zAxis, 0, zAxis.Length);
            }

            var leftToRead = recordSize - (
                4 + 32 + 4 +
                (numberOfPoints * 2 * 8) +
                ((numberOfAxes == 4) ? 16 + (numberOfPoints * 8) : 0));

            bool mValuesOptional = !(leftToRead == (16 + (8 * numberOfPoints)));

            byte[] measures = null;

            if (!mValuesOptional)
            {
                //skip the mbounds
                stream.Seek(16, SeekOrigin.Current);

                //read the measures
                measures = new byte[8 * numberOfPoints];
                stream.Read(measures, 0, measures.Length);
            }

            var point = new ShapeFilePoint(numberOfPoints, numberOfAxes);

            for (int i = 0; i < numberOfPoints; i++)
            {
                point[i * numberOfAxes] = ByteConverter.ToLittleEndianDouble(bytes, 8 * i * 2);
                point[(i + 1) * numberOfAxes] = ByteConverter.ToLittleEndianDouble(bytes, (8 * i * 2) + 8);

                if (numberOfAxes == 4)
                {
                    point[(i + 2) * numberOfAxes] = ByteConverter.ToLittleEndianDouble(zAxis, 8 * i);
                }

                if (numberOfAxes > 2)
                {
                    if (!mValuesOptional)
                    {
                        point[(i + (numberOfAxes == 3 ? 2 : 3)) * numberOfAxes] =
                            ByteConverter.ToLittleEndianDouble(measures, 8 * i);
                    }
                    else
                    {
                        point[(i + (numberOfAxes == 3 ? 2 : 3)) * numberOfAxes] = 0;
                    }
                }
            }
            return point;
        }

        private static ShapeFilePolyShape LoadPolyShape(Stream stream, long recordSize, int numberOfAxes, bool multiPatch)
        {
            //ignore the bounding box
            stream.Seek(32, SeekOrigin.Current);

            var bytes = new byte[8];

            stream.Read(bytes, 0, bytes.Length);

            var numberOfParts = ByteConverter.ToLittleEndianInt32(bytes, 0);
            var numberOfPoints = ByteConverter.ToLittleEndianInt32(bytes, 4);

            var test = BitConverter.ToInt32(bytes, 0);

            var parts = new byte[numberOfParts * 4];
            stream.Read(parts, 0, parts.Length);

            byte[] partTypes = null;

            if (multiPatch)
            {
                partTypes = new byte[numberOfParts * 4];
                stream.Read(partTypes, 0, partTypes.Length);
            }

            var points = new byte[numberOfPoints * 2 * 8];
            stream.Read(points, 0, points.Length);

            byte[] zAxis = null;

            //if we are are a 3D shape (+ possible M)
            if (numberOfAxes == 4)
            {
                //ignore the zbounds
                stream.Seek(16, SeekOrigin.Current);

                zAxis = new byte[numberOfPoints * 8];
                stream.Read(zAxis, 0, zAxis.Length);
            }

            bool mValuesOptional = true;

            if (numberOfAxes > 2)
            {
                var leftToRead = recordSize - (4 + 32 + 8 +
                    (numberOfParts * 4) +
                    ((multiPatch) ? (numberOfParts * 4) : 0) +
                    (numberOfPoints * 2 * 8) +
                    ((numberOfAxes == 4) ? (16 + (numberOfPoints * 8)) : 0));

                mValuesOptional = !(leftToRead == (16 + (8 * numberOfPoints)));
            }

            byte[] measures = null;

            if (!mValuesOptional)
            {
                //Ignore the mbounds
                stream.Seek(16, SeekOrigin.Current);

                measures = new byte[8 * numberOfPoints];
                stream.Read(measures, 0, measures.Length);
            }

            var polyShape = (multiPatch) ?
                new ShapeFileMultiPatch(numberOfParts) :
                new ShapeFilePolyShape(numberOfParts, numberOfAxes);

            for (int i = 0; i < numberOfParts; i++)
            {
                int startIndex = ByteConverter.ToLittleEndianInt32(parts, i * 4);
                int endIndex = 0;

                //if this is not the last part in the list the last point entry is from the partsList
                if (i != numberOfParts - 1)
                {
                    endIndex = ByteConverter.ToLittleEndianInt32(parts, (i + 1) * 4);
                }
                else //other wise take the last point
                {
                    endIndex = numberOfPoints;
                }

                var part = new double[(endIndex - startIndex) * numberOfAxes];

                for (int j = startIndex, index = 0; j < endIndex; j++, index++)
                {
                    part[index * numberOfAxes] = ByteConverter.ToLittleEndianDouble(points, j * 2 * 8);
                    part[(index * numberOfAxes) + 1] = ByteConverter.ToLittleEndianDouble(points, ((j * 2) + 1) * 8);

                    if (numberOfAxes == 4)
                    {
                        part[(index * numberOfAxes) + 2] = ByteConverter.ToLittleEndianDouble(zAxis, j * 8);
                    }

                    if (numberOfAxes > 2)
                    {
                        if (!mValuesOptional)
                        {
                            part[(index * numberOfAxes) + (numberOfAxes == 3 ? 2 : 3)] =
                                ByteConverter.ToLittleEndianDouble(measures, j * 8);
                        }
                        else
                        {
                            part[(index * numberOfAxes) + (numberOfAxes == 3 ? 2 : 3)] = 0;
                        }
                    }
                }

                if (multiPatch)
                {
                    ((ShapeFileMultiPatch)polyShape).SetPart(part, (ShapePartType)partTypes[i], i);
                }
                else
                {
                    polyShape.SetPart(part, i);
                }
            }

            return polyShape;
        }

        private static ShapeFilePoint LoadPoint(int numberOfPoints, int numberOfAxes, Stream stream)
        {
            var bytes = new byte[8 * numberOfPoints * numberOfAxes];

            stream.Read(bytes, 0, bytes.Length);

            var point = new ShapeFilePoint(numberOfPoints, numberOfAxes);

            for (int i = 0; i < (numberOfPoints * numberOfAxes); i++)
            {
                point[i] = ByteConverter.ToLittleEndianDouble(bytes, 8 * i);
            }

            return point;
        }

        #endregion Record Loading Methods

        #region Helper Methods

        private static bool IsZType(ShapeType shapeType)
        {
            switch (shapeType)
            {
                case ShapeType.PointZ:
                case ShapeType.PolylineZ:
                case ShapeType.PolygonZ:
                case ShapeType.MultiPointZ:
                case ShapeType.MultiPatch:
                    return true;

                case ShapeType.Unknown:
                case ShapeType.Null:
                case ShapeType.Point:
                case ShapeType.Polyline:
                case ShapeType.Polygon:
                case ShapeType.Multipoint:
                case ShapeType.PointM:
                case ShapeType.PolylineM:
                case ShapeType.PolygonM:
                case ShapeType.MultiPointM:
                default:
                    return false;
            }
        }

        private static bool IsMeasured(ShapeType shapeType)
        {
            switch (shapeType)
            {
                case ShapeType.PointZ:
                case ShapeType.PolylineZ:
                case ShapeType.PolygonZ:
                case ShapeType.MultiPointZ:
                case ShapeType.PointM:
                case ShapeType.PolylineM:
                case ShapeType.PolygonM:
                case ShapeType.MultiPointM:
                case ShapeType.MultiPatch:
                    return true;

                case ShapeType.Unknown:
                case ShapeType.Null:
                case ShapeType.Point:
                case ShapeType.Polyline:
                case ShapeType.Polygon:
                case ShapeType.Multipoint:
                default:
                    return false;
            }
        }

        #endregion Helper Methods

        /// <summary>
        /// Loads the ShapeFile into MapLayers
        /// </summary>
        /// <param name="shapeFileUri">The ShapeFile Uri</param>
        /// <param name="attributeFile">The Attribute File Uri (DBASE)</param>
        /// <param name="projectionFile">The Projection File (WKT)</param>
        /// <param name="recordComparison">Grouping logic</param>
        /// <returns>The grouped and loaded map layers</returns>
        public static MapLayer[] LoadLayers(
            Uri shapeFileUri,
            Uri attributeFile,
            Uri projectionFile,
            Func<ShapeFileRecordGraph, ShapeFileRecordGraph, bool> recordComparison)
        {
            if (shapeFileUri == null ||
                shapeFileUri.Scheme != Uri.UriSchemeFile ||
                !File.Exists(shapeFileUri.LocalPath))
                return null;

            using (var stream = File.Open(shapeFileUri.LocalPath, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                var shapeFile = ShapeFile.Read(stream, true);

                if (!IsSupportedGeometry(shapeFile.ShapeType))
                    return null;

                CoordinateReferenceSystem crs = LoadProjectionFile(projectionFile);

                LoadAttributes(attributeFile, shapeFile);

                var layerGroups = GroupLayers(shapeFile, recordComparison);

                var layers = new MapLayer[layerGroups.Count];

                for (int i = 0; i < layerGroups.Count; i++)
                {
                    List<PolyShape> polyshapes = new List<PolyShape>(layerGroups[i].Count);

                    for (int j = 0; j < layerGroups[i].Count; j++)
                    {
                        LoadShapeFileRecord(stream, shapeFile.Records[layerGroups[i][j]]);

                        var data = (ShapeFilePolyShape)shapeFile.Records[layerGroups[i][j]].Data;

                        var shapes = data.ToPolyShapes(GetEquivalentGeometry(shapeFile.Records[layerGroups[i][j]].ShapeType));

                        for (int k = 0; k < shapes.Length; k++)
                        {
                            shapes[k].SetAttributes(shapeFile.Records[layerGroups[i][j]].Attributes);
                        }

                        polyshapes.AddRange(shapes);
                    }

                    layers[i] = new PolyShapeMapLayer(new Guid(), "", polyshapes);

                    for (int j = 0; j < layerGroups[i].Count; j++)
                    {
                        //to avoid colisions the attributes are also added to the objects
                        layers[i].AddAttributes(shapeFile.Records[layerGroups[i][j]].Attributes);
                        layers[i].SetCoordinateReferenceSystem(crs, false);
                    }
                }

                return layers;
            }
        }

        private static List<List<int>> GroupLayers(
            ShapeFileGraph shapeFile,
            Func<ShapeFileRecordGraph, ShapeFileRecordGraph, bool> recordComparison)
        {
            var layerGroups = new List<List<int>>();

            if (recordComparison != null)
            {
                //group the layers
                var added = new List<bool>(shapeFile.Records.Count);

                for (int i = 0; i < shapeFile.Records.Count; i++)
                    added.Add(false);

                for (int i = 0; i < shapeFile.Records.Count; i++)
                {
                    if (added[i])
                        continue;

                    var group = new List<int>();
                    group.Add(i);
                    added[i] = true;

                    for (int j = 0; j < shapeFile.Records.Count; j++)
                    {
                        if (i == j || added[j])
                            continue;

                        if (recordComparison(shapeFile.Records[i], shapeFile.Records[j]))
                        {
                            group.Add(j);
                            added[j] = true;
                        }
                    }

                    layerGroups.Add(group);
                }
            }
            else
            {
                for (int i = 0; i < shapeFile.Records.Count; i++)
                {
                    layerGroups.Add(new List<int>() { i });
                }
            }

            return layerGroups;
        }

        private static DBaseGraph LoadAttributes(Uri attributeFile, ShapeFileGraph shapeFile)
        {
            if (attributeFile != null &&
                attributeFile.Scheme == Uri.UriSchemeFile &&
                File.Exists(attributeFile.LocalPath))
            {
                var attributes = DBaseFile.Read(attributeFile.LocalPath);

                if (attributes != null)
                {
                    //set the attributes to the shapefile record
                    for (int i = 0; i < shapeFile.Records.Count; i++)
                    {
                        shapeFile.Records[i].SetAttributes(DBaseGraph.ToGeometryAttributes(attributes, i));
                    }
                }
            }

            return null;
        }

        private static CoordinateReferenceSystem LoadProjectionFile(Uri projectionFile)
        {
            if (projectionFile != null &&
                projectionFile.Scheme == Uri.UriSchemeFile &&
                File.Exists(projectionFile.LocalPath))
                return CoordinateReferenceSystem.CreateFromWellKnownText(File.ReadAllText(projectionFile.LocalPath));

            return CoordinateReferenceSystem.LoadWgs84();
        }

        private static bool IsSupportedGeometry(ShapeType shapeType)
        {
            //currently only PolyLines, Polygons and MultiPoints are supported
            switch (shapeType)
            {
                case ShapeType.Polyline:
                case ShapeType.Polygon:
                case ShapeType.Multipoint:
                    return true;

                case ShapeType.PointZ:
                case ShapeType.PolylineZ:
                case ShapeType.PolygonZ:
                case ShapeType.MultiPointZ:
                case ShapeType.PointM:
                case ShapeType.PolylineM:
                case ShapeType.PolygonM:
                case ShapeType.MultiPointM:
                case ShapeType.MultiPatch:
                case ShapeType.Unknown:
                case ShapeType.Null:
                case ShapeType.Point:
                default:
                    return false;
            }
        }

        private static GeometryType GetEquivalentGeometry(ShapeType shapeType)
        {
            switch (shapeType)
            {
                case ShapeType.Unknown:
                case ShapeType.Null:
                    return GeometryType.Null;

                case ShapeType.Point:
                    return GeometryType.Point;

                case ShapeType.Polyline:
                    return GeometryType.Polyline;

                case ShapeType.Polygon:
                    return GeometryType.Polygon;

                case ShapeType.Multipoint:
                    return GeometryType.Multipoint;

                case ShapeType.PointZ:
                case ShapeType.PolylineZ:
                case ShapeType.PolygonZ:
                case ShapeType.MultiPointZ:
                case ShapeType.PointM:
                case ShapeType.PolylineM:
                case ShapeType.PolygonM:
                case ShapeType.MultiPointM:
                case ShapeType.MultiPatch:
                default:
                    return GeometryType.Null;
            }
        }
    }
}